/*
 * 代号：凤凰
 * http://www.jphenix.org
 * 2019年3月26日
 * V4.0
 */
package com.jphenix.one.tools;

import com.jphenix.standard.docs.ClassInfo;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * 获取当前CPU运行状况（CPU利用率）
 * import com.jphenix.one.tools.CPURunTime;
 * String[] res = CPURunTime.runtime();
 * 
 * 返回信息：0总的CPU利用率  1当前Java进程的CPU利用率  2当前进程号
 * 
 * String[] res = com.jphenix.one.tools.CPURunTime.runtime();
 * 
 * 目前只支持系统：windows（支持PowerShell），Linux（支持top命令）
 * 
 * 无需依赖第三方包
 * 
 * 2019-03-28 修改了在windows其他版本中取值错误，以及计算错误。
 * 2019-08-26 修改了获取系统信息方法，因为程序没有权限抛异常
 * 2019-12-26 修改了获取到的信息内容特殊，导致数组越界的问题
 * 2020-05-12 去掉了调用PowerShell没权限时报一大堆错的问题
 * 
 * @author MBG
 * 2019年3月26日
 */
@ClassInfo({"2020-05-12 19:39","获取当前CPU运行状况（CPU利用率）"})
public class CPURunTime {

	/**
	 * 获取系统CPU利用率
	 * @return  0总的CPU利用率  1当前进程CPU利用率  2当前进程号
	 * @throws Exception 异常
	 * 2019年3月26日
	 * @author MBG
	 */
	public static String[] runtime() {
		String pid = getPid(); //当前进程号
		//操作系统信息
		String os = System.getProperty("os.name");
		if(os==null) {
			return new String[]{"UNKNOWN","NULL",pid};
		}
		os = os.toLowerCase();
		if(os.indexOf("win")>-1) {
			//Windows系统获取CPU使用信息
			return winRuntime(pid);
		}else if(os.indexOf("linux")>-1) {
			//Linux系统获取CPU使用信息
			return linuxRuntime(pid);
		}
		//未知
		return new String[] {"UNKNOWN",os,pid};
		
	}
	
	/**
	 * Linux系统获取CPU使用信息
	 * 
	 * 获取CPU使用信息命令：top -bn 1
	 * 参数还是要加的，否则会一直输出CPU使用情况数据
	 * 
	 * 注意：linux中并不是\r\n，而是\n
	 * 
	 *        并没有写死要抓取指定字段位置，而是自动判断CPU占用率，PID
	 *        列的位置，避免不同版本的Linux输出信息中，有效列位置不同
	 * 
	 * @param pid 当前进程号
	 * @return  0CPU总利用率  1当前进程利用率  2当前进程号
	 * @throws Exception 异常
	 * 2019年3月26日
	 * @author MBG
	 */
	private static String[] linuxRuntime(String pid) {
		String[] texts; //调用命令获取到的信息
		try {
			//执行获取CPU使用信息命令
			Process proc = Runtime.getRuntime().exec("top -bn 1");
			//构建输出信息写入流
			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			//构建输出信息读取流
			InputStream is = proc.getInputStream();
			int readCount; //读取信息量
			byte[] buffer = new byte[1024]; //缓存
			while((readCount=is.read(buffer))>-1) {
				bos.write(buffer,0,readCount);
			}
			//构建输出信息
			texts = bos.toString("UTF-8").split("\n");
		}catch(Exception e) {
			//通常都是没有权限导致的
			//e.printStackTrace();
			//return new String[] {"Exception",e.toString(),""};
			return new String[] {"","",""};
		}
		Integer[] headerPoints = null; //头信息分割点
		double totalCpu        = 0;    //总共CPU占用率
		String curCpu          = "";   //当前进程CPU占用率
		String pidEle;                 //进程id值
		String cpuEle;                 //CPU占用率值
		int    cpuPoint        = 0;    //cpu利用率字段位置
		int    pidPoint        = 0;    //pid值字段位置
		//处理返回报文
		for(int i=0;i<texts.length;i++) {
			if(texts[i].trim().startsWith("PID")) {
				//注意PID左侧是有空格的（表头跟数据都是右侧对齐）
				// 0   1        2   3      4      5       6  7  8    9        10     11
				//PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
				Object[] res = parseHeader(texts[i]);
				headerPoints = (Integer[])res[1];
				String[] fields = (String[])res[0]; //字段数组
				for(int j=0;j<fields.length;j++) {
					if("PID".equals(fields[j])) {
						pidPoint = j;
					}else if("%CPU".equals(fields[j])) {
						cpuPoint = j;
					}
				}
				continue;
			}
			if(headerPoints==null) {
				continue;
			}
			cpuEle = texts[i].substring(cpuPoint==0?0:headerPoints[cpuPoint-1],headerPoints[cpuPoint]).trim();
			pidEle = texts[i].substring(pidPoint==0?0:headerPoints[pidPoint-1],headerPoints[pidPoint]).trim();
			
			totalCpu += parseDouble(cpuEle);
			
			if(pidEle.equals(pid)) {
				curCpu = cpuEle;
			}
		}
		//数值格式化类
		DecimalFormat df = new DecimalFormat("#0.0");
		
		return new String[] {df.format(totalCpu),curCpu,pid};
	}
	
	
	/**
	 * Windows系统获取CPU使用信息 【注意：操作系统需要支持PowerShell】
	 * 
	 * PowerShell 获取进程耗费CPU资源情况的命令：Get-Process
	 * 
	 * windows只返回进程使用了CPU资源的时间，单位为秒，小数点保留后两位。
	 * 计算CPU占用率：间隔1秒取两次数据，比对两次数据的CPU使用时间差额，
	 * 如果差额为0，说明这一秒内，这个进程没有使用CPU资源，也就是CPU占用
	 * 率为0%，如果为1秒，那么这一秒CPU只执行了这个进程，那么CPU占用率为
	 * 100%。
	 * 将这一秒之间所有进程使用CPU资源的时间累加的和，就是这一秒的CPU资源
	 * 使用总时间，通常都小于1秒（极端情况计算错误时可能会大于1秒），小数点
	 * 还保留了2位，乘以100，就是CPU占用率
	 * 
	 * 注意：在计算CPU占用率时，去掉了PowerShell进程占用CPU的时间，因为没人
	 * 在意这一秒，干这事儿的PowerShell耗时多少。
	 * 
	 *  另外，这个数值也不是绝对的，没找到（没认真找）windows中能直接返回CPU
	 *  占用率的命令，也没有找到间隔1秒返回两次数据的传入参数。
	 *  
	 *  只是在程序中设置等待1秒再次获取数量。实际上还包括了操作系统调用PowerShell
	 *  解析程序，加载并解析获取CPU执行信息的脚本，将信息写入输出流等动作，都会
	 *  耗费一定的时间。
	 *  
	 *  注意：java执行命令行时，有的操作系统（比如：win10）直接写PowerShell Get-Process
	 *        就可以，有些操作系统，比如早期版本，光这写，读取输出数据时会导致阻塞，
	 *        即使加了proc.getOutputStream().close();也不好使。最后发现在命令行
	 *        后面加上\r\n好使了。
	 *        
	 *   注意：windows不同版本输出进程信息的内容可能不同。比如虚拟机中的操作系统，
	 *         会多出VM列，天知道什么版本的操作系统会多出什么列，最后将程序改成
	 *         了动态判断CPU占用率数据列位置，PID列位置。
	 * 
	 * @param  pid 当前进程号
	 * @return 0CPU总利用率  1当前进程利用率  2当前进程号
	 * @throws Exception 异常
	 * 2019年3月26日
	 * @author MBG
	 */
	private static String[] winRuntime(String pid) {
		ByteArrayOutputStream bos;      //构建接收流（第一次）
		String[] texts;                 //调用命令获取到的信息（第一次）
		String[] texts2;                //调用命令获取到的信息（第二次）
		int readCount;                  //读取量
		InputStream is = null;          //构建信息输出流
		byte[] buffer = new byte[1024]; //缓存
		try {
			/* 第一次执行 */
			//构建执行进程
			Process proc = Runtime.getRuntime().exec("PowerShell Get-Process\r\n");
			long usedTime = System.currentTimeMillis(); //执行第一次所耗费的时间
			proc.getOutputStream().close();
			proc.getErrorStream().close();
			//构建接收流
			bos = new ByteArrayOutputStream();
			is = proc.getInputStream();
			while((readCount=is.read(buffer))>-1) {
				bos.write(buffer,0,readCount);
			}
			is.close();
			texts  = bos.toString("GBK").split("\r\n");
			usedTime = System.currentTimeMillis()-usedTime;
			if(usedTime<1000) {
				Thread.sleep(1000-usedTime); //暂停1秒，除去第一次耗费的时间
			}
			
			/* 第二次执行 */
			proc = Runtime.getRuntime().exec("PowerShell Get-Process");
			proc.getOutputStream().close();
			proc.getErrorStream().close();
			//构建接收流
			bos = new ByteArrayOutputStream();
			//构建信息输出流
			is = proc.getInputStream();
			while((readCount=is.read(buffer))>-1) {
				bos.write(buffer,0,readCount);
			}
			texts2 = bos.toString("GBK").split("\r\n");
		}catch(Exception e) {
			//e.printStackTrace();
			System.out.println("Run PowerShell Exception:["+e.toString()+"]");
			return new String[] {"Exception",e.toString(),""};
		}finally {
			try {
				is.close();
			}catch(Exception e2) {}
		}
		/*
		 * Handles: 进程打开的句柄数.
		 * NPM(K): 进程正在使用的非分页内存量, 单位KB(kilobytes).
		 * PM(K): 进程正在使用的可分页的内存量, 单位KB(kilobytes).
		 * WS(K): 进程工作集的大小, 单位KB(kilobytes). 工作集包括进程最近引用的内存的页面
		 * VM(M): 进程正在使用的虚拟内存量, 单位MB(megabytes). 虚拟内存包括磁盘上分页文件中的存储.
		 * CPU(s): 进程在所有的处理器上运行的时间, 单位是秒.
		 * ID: 进程的进程ID (PID).
		 * ProcessName: 进程名称.
		 */
		String[]  fields       = null;  //字段元素数组
		Integer[] headerPoints = null;  //头信息分割点
		double    cpuEle;               //cpu利用率值
		String    pidEle;               //进程id值
		String    procName;             //进程名
		int       cpuPoint     = 0;     //cpu利用率字段位置
		int       pidPoint     = 0;     //pid值字段位置
		int       namePoint    = 0;     //进程名位置
		List<String> pidList = new ArrayList<String>(); //PID序列
		Map<String,Double> cpuMap = new HashMap<String,Double>(); //CPU使用时间容器
		Object[] res;                   //解析表头信息
		for(int i=0;i<texts.length;i++) {
			if(texts[i].trim().startsWith("Handles")) {
				//  0        1        2          3         4          5   6      7
				//Handles  NPM(K)    PM(K)      WS(K)     CPU(s)     Id  SI ProcessName
				
				//次奥的，居然还多了一个VM(M) 需要
				//  0        1        2          3     4        5         6     7
				//Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
				res             = parseHeader(texts[i]);
				headerPoints    = (Integer[])res[1];
				fields          = (String[])res[0];
				
				for(int j=0;j<fields.length;j++) {
					if("cpu(s)".equalsIgnoreCase(fields[j])) {
						cpuPoint = j;
					}else if("id".equalsIgnoreCase(fields[j])) {
						pidPoint = j;
					}else if("processname".equalsIgnoreCase(fields[j])) {
						namePoint = j;
					}
				}
				continue;
			}
			if(texts[i].length()<7 || texts[i].startsWith("-------")) {
				//无用行
				continue;
			}
			if(headerPoints!=null && texts2.length>i) {
				//通常进程名都放在末尾
				procName = texts2[i].substring(namePoint==0?0:headerPoints[namePoint-1]).trim().toLowerCase();
				if("powershell".equals(procName)) {
					//除去当前运行的获取CPU资源的程序
					continue;
				}
				cpuEle = parseDouble(texts[i].substring(cpuPoint==0?0:headerPoints[cpuPoint-1],headerPoints[cpuPoint]).trim());
				pidEle = texts[i].substring(pidPoint==0?0:headerPoints[pidPoint-1],headerPoints[pidPoint]).trim();
				
				pidList.add(pidEle);
				cpuMap.put(pidEle,cpuEle);
			}
		}
		double totalCpu = 0; //总共CPU占用率
		double fCpu;         //上一秒CPU使用时间
		Double cpuVal;       //CPU使用值
		headerPoints = null;
		//处理第二次获取信息计算差额
		for(int i=0;i<texts2.length;i++) {
			if(texts2[i].trim().startsWith("Handles")) {
				res          = parseHeader(texts2[i]);
				headerPoints    = (Integer[])res[1];
				fields          = (String[])res[0];
				for(int j=0;j<fields.length;j++) {
					if("cpu(s)".equalsIgnoreCase(fields[j])) {
						cpuPoint = j;
					}else if("id".equalsIgnoreCase(fields[j])) {
						pidPoint = j;
					}else if("processname".equalsIgnoreCase(fields[j])) {
						namePoint = j;
					}
				}
				continue;
			}
			if(texts2[i].length()<7 || texts2[i].trim().startsWith("-------")) {
				//无用行
				continue;
			}
			if(headerPoints!=null) {
				//通常进程名都放在末尾
				procName = texts2[i].substring(namePoint==0?0:headerPoints[namePoint-1]).trim().toLowerCase();
				if("powershell".equals(procName)) {
					//除去当前运行的获取CPU资源的程序
					continue;
				}
				cpuEle = parseDouble(texts2[i].substring(cpuPoint==0?0:headerPoints[cpuPoint-1],headerPoints[cpuPoint]).trim());
				pidEle = texts2[i].substring(pidPoint==0?0:headerPoints[pidPoint-1],headerPoints[pidPoint]).trim();
				
				if(pidList.contains(pidEle)) {
					fCpu = cpuMap.get(pidEle);
					cpuVal = cpuEle-fCpu;
					if(cpuVal==null || cpuVal<0) {
						//该进程已经释放
						continue;
					}
				}else {
					//上一秒还没有这个进程，这一秒就有了，不做任何处理，因为可能上一次没有取到（为啥没取到不清楚）
					continue;
				}
				cpuMap.put(pidEle,cpuVal); //更新为CPU使用值
				totalCpu += cpuVal;
			}
		}
		//因为两次执行的间隔实际上是超过1秒的，导致类加的数可能会超过1，最后计算结果会大于100%。
		if(totalCpu>1) {
			totalCpu = 1d;
		}
		//数值格式化类 windows跟linux都一样，占用率都是小数点保留1位
		DecimalFormat df = new DecimalFormat("#0.0");
		cpuVal = cpuMap.get(pid); //当前进程的CPU使用值
		return new String[] {df.format(totalCpu*100)+"%",cpuVal==null?"NaN":df.format(cpuVal*100)+"%",pid};
	}
	
	/**
	 * 解析表头返回数据分割点
	 * 
	 * windows和linux表头跟数据都是右侧对齐的，需要解析出表头
	 * 结束位置，按照这个位置分割数据。并不能单纯的按照空格做
	 * 分割，因为每一列的空格的数量都不是固定的。
	 * 
	 * 另外，表头在解析之前，不能做trim去掉左侧的空格（因为跟
	 * 数据一样，都是采用右侧对齐，左侧用空格补充）
	 * 
	 * 注意：在linux系统中，表头专门有个字段S，这个可恨的字段
	 * 只有一个字母，而且这个字母还存在于其它字段单词中，导致
	 * 解析数值位置时返回错误的位置
	 * 
	 * 解决办法：将表头字符串前后加上空格，字段名前后也加上空格
	 * 
	 * @param header 表头
	 * @return       信息数组  0字段名数组   1字段起始位置数组
	 * 2019年3月26日
	 * @author MBG
	 */
	private static Object[] parseHeader(String header) {
		String[] ele = header.split(" "); //分割为列数组
		//处理后的表头返回数据分割点序列
		List<String>  eleList      = new ArrayList<String>();  //字段名序列
		List<Integer> elePointList = new ArrayList<Integer>(); //字段起始位置序列
		for(int i=0;i<ele.length;i++) {
			ele[i] = ele[i].trim();
			if(ele[i].length()<1) {
				continue;
			}
			eleList.add(ele[i]);
			elePointList.add((" "+header+" ").indexOf(" "+ele[i]+" ")+ele[i].length());
		}
		//构建返回值
		String[]  res0 = new String[eleList.size()];
		eleList.toArray(res0);
		Integer[] res1 = new Integer[elePointList.size()];
		elePointList.toArray(res1);
		return new Object[] {res0,res1};
	}
	
	
	/**
	 * 解析字符串中的数字
	 * windows系统中，数字带千位分隔符比如 3,100.01 导致字符串转换数字时报错
	 * 在这里需要去掉分隔符
	 * @param douStr 字符串型数字
	 * @return 整理后的数字
	 * 2019年3月26日
	 * @author MBG
	 */
	private static double parseDouble(String douStr) {
		if(douStr.length()<1) {
			return 0d;
		}
		//妈的，居然发现win7的powershell返回的cpu运行时间的值居然有这样的：...28.77
		while(douStr.startsWith(".")) {
			douStr = douStr.substring(1);
		}
		int point; //分割点
		while((point=douStr.indexOf(","))>-1) {
			if(point==0) {
				douStr = douStr.substring(1);
			}else if(point==douStr.length()-1){
				douStr = douStr.substring(0,point);
			}else {
				douStr = douStr.substring(0,point)+douStr.substring(point+1);
			}
		}
		double dou = Double.parseDouble(douStr);
		return dou;
	}
	
    /**
     * 获取当前进程号
     * @return 当前进程号
     * 2016年1月25日
     * @author 马宝刚
     */
    public static String getPid() {
        //当下语句针对windows操作系统
        String name = ManagementFactory.getRuntimeMXBean().getName();
        // get pid    
        String pid = name.split("@")[0];    
        return pid;
    }
}
